
#include <map>
#include <algorithm>

#include "OpenGL.h"
#include "Camera.h"
#include "Mouse.h"
#include "GraphicsUtility.h"
#include "Vector.h"
#include "TriangleMesh.h"
#include "MeshRenderer.h"
#include "Drawable.h"

using namespace Velyn;
using namespace OhioState::Math;

class SmoothingOperator {
    
private:
    
    // map midpoints to existing vertices in the mesh
    map<int, VertexIndex> midpointIndices;
  
    Texture calculateMidpoint(Texture texture0, Texture texture1) {
        return (texture0 + texture1) * 0.5;
    }
    
    // hash the vertex indicies
    int getHashKey(int point0, int point1, int hashMultiplier) {
        int key;
        
        if(point0 < point1) {
            key = hashMultiplier * point0 + point1;
        } else {
            key = hashMultiplier * point1 + point0;
        }
        
        return key;
    }
    
public:
    
    SmoothingOperator() {}
    
    ~SmoothingOperator() {}
    
    TriangleMesh* smoothSurface(TriangleMesh* mesh) {
        
        // copy over the vertices, normals, and textures
        TriangleMesh* newMesh = new TriangleMesh(*mesh);
        
        // clear the map
        midpointIndices.clear();
        
        // for each triangle
        for(int triangle = 0; triangle < mesh->numberTriangles(); triangle++) {
            
            // get the vertices for the triangle
            VertexList vertices = mesh->getVertices(triangle);
            
            // get the indicies of the vertex indices for the triangle
            VertexIndexList vertexIndex = mesh->getVertexIndices(triangle);
            
            // get the normals for the triangle
            NormalList normals = mesh->getNormals(triangle);
            
            // get the textures
            TextureList textures = mesh->getTextures(triangle);
            
            int hashKey;
            int midpointIndex[3];
            
            // NormalList normals = mesh->calculateNormals(triangle);
            
            map<int, int>::const_iterator findIterator;
            
            // hash the set of vertices and see if they exist in the map
            // if they do then use the index in the map
            // if they do not exist then create the midpoint and map it to that index
            
            // for midpoint 1
            // get the hash key
            hashKey = getHashKey(vertexIndex[0], vertexIndex[1], mesh->numberVertices());
            
            // check if it is in the map
            findIterator = midpointIndices.find(hashKey);
            
            // if the key does not exist
            if(findIterator == midpointIndices.end()) {
                Vertex midpoint;
                Normal normal;
                Texture texture;

                float fU = 0.0f;
                float fV = 0.5f;
                float fW = 0.5f;
                
                Vector3 projection[3];
                
                midpoint = (vertices[0] * fW) + (vertices[1] * fV) + (vertices[2] * fU);
                
                projection[0] = midpoint - (midpoint-vertices[0]).dot(normals[0]) * normals[0];
                projection[1] = midpoint - (midpoint-vertices[1]).dot(normals[1]) * normals[1];
                projection[2] = midpoint - (midpoint-vertices[2]).dot(normals[2]) * normals[2];
                
                midpoint = (projection[0] * fW) + (projection[1] * fV) + (projection[2] * fU);
                
                normal = (normals[0] * fW) + (normals[1] * fV) + (normals[2] * fU);
                
                midpointIndex[0] = newMesh->addVertex(midpoint);
                
                newMesh->addNormal(normal);
                
                // calculate the texture
                texture = calculateMidpoint(textures[0], textures[1]);
                
                // add the new texture
                newMesh->addTexture(texture);
                
                midpointIndices[hashKey] = midpointIndex[0];
                
            } else {
                
                // if the key does exist, get its index from the map
                midpointIndex[0] = findIterator->second;
                // cout << "Founded Index at: " << midpointIndex[1] << endl;
            }
            
            // repeat for midpoint 2
            hashKey = getHashKey(vertexIndex[1], vertexIndex[2], mesh->numberVertices());
            findIterator = midpointIndices.find(hashKey);
            
            if(findIterator == midpointIndices.end()) {
                Vertex midpoint;
                Normal normal;
                Texture texture;
                
                float fU = 0.5f;
                float fV = 0.5f;
                float fW = 0.0f;
                
                Vector3 projection[3];
                
                midpoint = (vertices[0] * fW) + (vertices[1] * fV) + (vertices[2] * fU);
                
                projection[0] = midpoint - (midpoint-vertices[0]).dot(normals[0]) * normals[0];
                projection[1] = midpoint - (midpoint-vertices[1]).dot(normals[1]) * normals[1];
                projection[2] = midpoint - (midpoint-vertices[2]).dot(normals[2]) * normals[2];
                
                midpoint = (projection[0] * fW) + (projection[1] * fV) + (projection[2] * fU);
                
                normal = (normals[0] * fW) + (normals[1] * fV) + (normals[2] * fU);
                
                midpointIndex[1] = newMesh->addVertex(midpoint);
                
                newMesh->addNormal(normal);
                
                texture = calculateMidpoint(textures[1], textures[2]);
                newMesh->addTexture(texture);
                
                midpointIndices[hashKey] = midpointIndex[1];
                
            } else  {
                midpointIndex[1] = findIterator->second;
                // cout << "Founded Index at: " << midpointIndex[1] << endl;
            }
            
            // and for midpoint 3
            hashKey = getHashKey(vertexIndex[2], vertexIndex[0], mesh->numberVertices());
            findIterator = midpointIndices.find(hashKey);
            
            if(findIterator == midpointIndices.end()) {
                Vertex midpoint;
                Normal normal;
                Texture texture;
        
                float fU = 0.5f;
                float fV = 0.0f;
                float fW = 0.5f;
                
                Vector3 projection[3];
                
                midpoint = (vertices[0] * fW) + (vertices[1] * fV) + (vertices[2] * fU);
                
                projection[0] = midpoint - (midpoint-vertices[0]).dot(normals[0]) * normals[0];
                projection[1] = midpoint - (midpoint-vertices[1]).dot(normals[1]) * normals[1];
                projection[2] = midpoint - (midpoint-vertices[2]).dot(normals[2]) * normals[2];
                
                midpoint = (projection[0] * fW) + (projection[1] * fV) + (projection[2] * fU);
                
                normal = (normals[0] * fW) + (normals[1] * fV) + (normals[2] * fU);
                
                midpointIndex[2] = newMesh->addVertex(midpoint);
                
                newMesh->addNormal(normal);
                
                texture = calculateMidpoint(textures[2], textures[0]);
                newMesh->addTexture(texture);
                
                midpointIndices[hashKey] = midpointIndex[2];
                
            } else {
                midpointIndex[2] = findIterator->second;
                // cout << "Founded Index at: " << midpointIndex[1] << endl;
            }
            
            // FIXME: this is not mapping correctly
            // add triangles to newMesh in clockwise order
            newMesh->addTriangle(vertexIndex[0], midpointIndex[0], midpointIndex[2]);
            newMesh->addTriangle(midpointIndex[0], vertexIndex[1], midpointIndex[1]);
            newMesh->addTriangle(midpointIndex[0], midpointIndex[1], midpointIndex[2]);
            newMesh->addTriangle(midpointIndex[2], midpointIndex[1], vertexIndex[2]);
        }
        
        // swap the addresses of mesh and newMesh
        // std::swap(mesh, newMesh);
        
        delete mesh;
        
        return newMesh;
    }
};

//-------------------------------------------------------------------------------
// Prototypes
//-------------------------------------------------------------------------------

void display();
void keyboardPress(unsigned char key, int x, int y);
void keyboardRelease(unsigned char key, int x, int y);
void reshape(int w, int h);
void timer(int extra);
void initialize();
void mouseButtonPress(int button, int state, int x, int y);
void mouseMotion(int x, int y);

//-------------------------------------------------------------------------------
// Globals
//-------------------------------------------------------------------------------

Camera camera(4.0f);
Mouse mouse;

Vector3 midpoints[3];
// Vector3 IPosition[3], INormal[3];
Vertex IPosition[3];
Normal INormal[3];

Vector3 f3Position[3], f3Normal[3];
Vector3 projection[3];
Vector3 triangleCenter;

TriangleMesh* mesh;
IDrawable* drawable;

//-------------------------------------------------------------------------------
// Display
//-------------------------------------------------------------------------------

void display() {
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	camera.enable();
    mouse.rotateObject();
    GraphicsUtility::compass();
    
    glPointSize(5.0f);
    
    // render the point in the triangle
    
    VertexList vertices = mesh->getVertexRecord();
    
    glBegin(GL_POINTS);
    
    for(int vertex = 0; vertex < vertices.size(); vertex++) {
        glColor3f(1.0f, 0.0f, 0.0f);
        glVertex3fv(vertices[vertex].getValue());
    }
    
    glEnd();
    
    glutSwapBuffers();
}

//-------------------------------------------------------------------------------
// Keyboard Press
//-------------------------------------------------------------------------------

void keyboardPress(unsigned char key, int x, int y) {
	switch(key) {
		case 27: {
            delete mesh;
            delete drawable;
			exit(0);
		} break;
			
		case '`': {
		} break;
			
		case 'a': {
            SmoothingOperator smoothingOperator;
            
            mesh = smoothingOperator.smoothSurface(mesh);
            
            cout << "Number of Vertices: " << mesh->numberVertices() << endl;
            
        } break;
            
		case 'w': {	
		} break;
            
		default: {
		} break;
	}
}

//-------------------------------------------------------------------------------
// Keyboard Release
//-------------------------------------------------------------------------------

void keyboardRelease(unsigned char key, int x, int y) {}

//-------------------------------------------------------------------------------
// Reshape
//-------------------------------------------------------------------------------

void reshape(int w, int h) {
	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, 1.6, 1.0, 200.0);
}

//-------------------------------------------------------------------------------
// Timer
//-------------------------------------------------------------------------------

void timer(int extra) {
	glutPostRedisplay();
	glutTimerFunc(40, timer, 0);
}

//-------------------------------------------------------------------------------
// Initialize
//-------------------------------------------------------------------------------

// Phong Tessellation
// http://www.gamedev.net/topic/565722-help-with-phong-tessellation/
// http://www.youtube.com/watch?v=9s0onJyTmWE
// http://http.developer.nvidia.com/GPUGems3/gpugems3_ch05.html
// http://developer.download.nvidia.com/presentations/2010/gdc/NVIDIA_Tessellation_GDC10.pdf
// http://onrendering.blogspot.com/2011/12/tessellation-on-gpu-curved-pn-triangles.htmlb
// http://perso.telecom-paristech.fr/~boubek/papers/PhongTessellation/PhongTessellation.pdf

// Barycentric Coordinates
// http://blogs.msdn.com/b/rezanour/archive/2011/08/07/barycentric-coordinates-and-point-in-triangle-tests.aspx
// http://crackthecode.us/barycentric/barycentric_coordinates.html
// http://www.farinhansford.com/dianne/teaching/cse470/materials/BarycentricCoords.pdf

// Calculate Barycentric Coordinates
// http://www.gamedev.net/topic/621445-barycentric-coordinates-c-code-check/

void initialize() {
    
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	glShadeModel(GL_SMOOTH);
    
    // FIXME: handle the drawables in TriangleMesh
    // ie. mesh = new TriangleMesh(TEST_TRIANGLE);
    // or it can just be a regular mesh, mesh = new TriangleMesh();
    drawable = new TestTriangle();
    mesh = drawable->draw();
    
    glEnable(GL_DEPTH_TEST);
	glEnable(GL_NORMALIZE);
    
	glMatrixMode(GL_PROJECTION);		
	glLoadIdentity();
	gluPerspective(45.0, 1.6, 1.0, 200.0);
}

//-------------------------------------------------------------------------------
// Mouse Press
//-------------------------------------------------------------------------------

void mouseButtonPress(int button, int state, int x, int y) {
    mouse.pressEvent(button, state, x, y);
}

//-------------------------------------------------------------------------------
// Mouse Motion
//-------------------------------------------------------------------------------

void mouseMotion(int x, int y) {
    mouse.motionEvent(x, y);
}

//-------------------------------------------------------------------------------
// Main
//-------------------------------------------------------------------------------

int main(int argc, char** argv) {
	
    glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
	glutInitWindowSize(800, 500);
	glutInitWindowPosition(100, 100);
	glutCreateWindow("Smooth Operator Test");
	initialize();
	
	glutKeyboardFunc(keyboardPress);
	glutKeyboardUpFunc(keyboardRelease);
	
	glutMouseFunc(mouseButtonPress);
	glutMotionFunc(mouseMotion);
	
	glutTimerFunc(25, timer, 0);
	glutDisplayFunc(display);
	
	glutReshapeFunc(reshape);
    
	glutMainLoop();
}
